
package client;

import com.jme3.app.Application;
import com.jme3.app.state.AppState;
import com.jme3.app.state.AppStateManager;
import com.jme3.input.RawInputListener;
import com.jme3.input.event.JoyAxisEvent;
import com.jme3.input.event.JoyButtonEvent;
import com.jme3.input.event.KeyInputEvent;
import com.jme3.input.event.MouseButtonEvent;
import com.jme3.input.event.MouseMotionEvent;
import com.jme3.math.Vector2f;
import com.jme3.math.Vector3f;
import com.jme3.renderer.RenderManager;
import com.jme3.system.AppSettings;
import com.jme3.system.JmeCanvasContext;
import common.Client;
import common.packet.ClientInfo;
import common.packet.Input;
import common.packet.WorldState;
import java.awt.Canvas;
import java.awt.Point;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.BlockingQueue;
import java.util.concurrent.Callable;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.LinkedBlockingQueue;
import org.jbox2d.common.Vec2;

/**
 * Holds stuff about the game, specially the match itself. It owns the physics
 * world, the graphics world and the collection of clients.
 * 
 * @author xissburg
 */
public class Game
{
    private Client client;
    private PhysicsThreadC physicsThread;
    private SendInputThread sendInputThread;
    private GraphicsWorld graphicsWorld;
    private Map<String, ClientInfo> clients;

    public Game(Client client)
    {
        this.client = client;
        clients = new ConcurrentHashMap<String, ClientInfo>();
        physicsThread = new PhysicsThreadC(this);
        sendInputThread = new SendInputThread();

        graphicsWorld = new GraphicsWorld();
        graphicsWorld.setPauseOnLostFocus(false);

        AppSettings settings = new AppSettings(true);
        settings.setFrameRate(60);
        graphicsWorld.setSettings(settings);

        graphicsWorld.enqueue(new Callable<Void> () {
            public Void call() throws Exception {
                graphicsWorld.getInputManager().
                        addRawInputListener(new SetPositionListener());
                return null;
            }
        });
    }

    /**
     * Initializes and returns the JmeCanvas
     *
     * @return JmeCanvas
     */
    public Canvas createCanvas()
    {
        graphicsWorld.createCanvas();
        JmeCanvasContext context = (JmeCanvasContext)graphicsWorld.getContext();
        return context.getCanvas();
    }

    /**
     * Adds a button for a client. It adds the button to the graphicsWorld and 
     * set state calling setClientState
     * 
     * @param clientInfo
     */
    public void addButton(ClientInfo clientInfo)
    {
        //clientInfo = clientInfo.copy();
        String name = clientInfo.getName();
        clients.put(name, clientInfo);

        //Only players are added to the graphics and physics worlds
        if(clientInfo.getType() == ClientInfo.Type.PLAYER)
        {
            float x = clientInfo.getStartX(), y = clientInfo.getStartY();
            graphicsWorld.addButton(name, x, y);
            physicsThread.getPhysicsWorld().addButton(name, x, y, clientInfo.getTeam());
        }
    }

    /**
     * Removes the button of a given client from both the physics and graphics
     * worlds.
     * 
     * @param clientInfo
     */
    public void removeButton(String name)
    {
        ClientInfo clientInfo = clients.get(name);
        clients.remove(name);

        if(clientInfo.getType() == ClientInfo.Type.PLAYER)
        {
            graphicsWorld.removeNode(name);
            physicsThread.getPhysicsWorld().removeBody(name);
        }
    }

    public Collection<ClientInfo> getClients() {
        return new java.util.ArrayList<ClientInfo>(clients.values());
    }

    /**
     * Updates the state of the button of an existing client.
     * 
     * @param clientState
     */
    public void setClientInfo(ClientInfo ci)
    {
        String name = ci.getName();
        ClientInfo clientInfo = clients.get(name);

        //if it was a player and continues to be a player, create the button if
        //not yet existent and then update its position
        if(clientInfo.getType() == ClientInfo.Type.PLAYER &&
           ci.getType() == ClientInfo.Type.PLAYER)
        {
            float x = ci.getStartX();
            float y = ci.getStartY();
            clientInfo.setStartXY(x, y);
            clientInfo.setTeam(ci.getTeam());
            physicsThread.getPhysicsWorld().setBodyPosition(name, x, y);
            graphicsWorld.setButtonPosition(name, x, y);
        }
        //if it was a player and now it is a spectator, remove from physicsWorld
        //and graphicsWorld
        else if(clientInfo.getType() == ClientInfo.Type.PLAYER &&
                ci.getType() == ClientInfo.Type.SPECTATOR)
        {
            clientInfo.setType(ClientInfo.Type.SPECTATOR);
            physicsThread.getPhysicsWorld().removeBody(name);
            graphicsWorld.removeNode(name);
        }
        //if it was a spectator and now it is a player, add it to the physicsWorld
        //and change its state in the physicsWorld and update its starting position
        else if(clientInfo.getType() == ClientInfo.Type.SPECTATOR &&
                ci.getType() == ClientInfo.Type.PLAYER)
        {
            clientInfo.setType(ClientInfo.Type.PLAYER);
            float x = ci.getStartX(), y = ci.getStartY();
            clientInfo.setStartXY(x, y);
            clientInfo.setTeam(ci.getTeam());
            physicsThread.getPhysicsWorld().addButton(name, x, y, clientInfo.getTeam());
            graphicsWorld.addButton(name, x, y);
        }
        //else if it was a spectator and wants to be a spectator there's nothing
        //to do.
    }


    public void setWorldState(WorldState worldState)
    {
        physicsThread.setPhysicsInput(worldState);
    }

    public ClientInfo getClientInfo(String name) {
        return clients.get(name).copy();
    }

    /**
     * Stops the game. Releases all its resources and stops all of it's threads.
     */
    public void stop()
    {
        physicsThread.stop();
        sendInputThread.stop();
        graphicsWorld.stop(true);
    }

    /**
     * Starts the match. Removes the SetPositionListener from the graphicsWorld.
     * inputManager and adds a CursorDisplacementListener to it, and starts the
     * physics thread.
     */
    public void start()
    {
        final CursorDisplacementListener cdl = 
                new CursorDisplacementListener(sendInputThread);

        graphicsWorld.enqueue(new Callable<Void> () {
            public Void call() throws Exception {
                graphicsWorld.getInputManager().clearRawInputListeners();

                if(client.getClientInfo().getType() == ClientInfo.Type.PLAYER)
                    graphicsWorld.getInputManager().addRawInputListener(cdl);

                graphicsWorld.getStateManager().attach(new AppState() {
                    public boolean isInitialized() {
                        return true;
                    }

                    public void update(float tpf)
                    {
                        Map<String, float[]> posMap = null;

                        try {
                            posMap = physicsThread.getPhysicsOutput(false);
                        } catch (InterruptedException ex) {}

                        if(posMap != null)
                        {
                            Set<Map.Entry<String, float[]>> posEntrySet = posMap.entrySet();

                            for(Map.Entry<String, float[]> entry: posEntrySet)
                            {
                                float[] pos = entry.getValue();
                                graphicsWorld.setNodePosition(entry.getKey(),
                                        new Vector3f(pos[0], 0.1f, pos[1]), true);
                            }
                        }
                    }

                    public void initialize(AppStateManager stateManager, Application app) {}
                    public void stateAttached(AppStateManager stateManager) {}
                    public void stateDetached(AppStateManager stateManager) {}
                    public void render(RenderManager rm) {}
                    public void cleanup() {}
                });
                
                return null;
            }
        });

        new Thread(physicsThread, "Physics Thread").start();

        if(client.getClientInfo().getType() == ClientInfo.Type.PLAYER)
            new Thread(sendInputThread, "Send Input Thread").start();
    }


    /*
     * Inner Classes
     * These classes are inner classes because they don't make sense outside of
     * here. They are really part of the Game so they must be coupled with it.
     */

    /**
     * RawInputListeners
     */

    /**
     * Raw listener that handles set starting position requests (to be set for
     * mouse clicking action). It first checks whether its possible to set this
     * position by checking intersections with other objects. If it doesn't
     * instersect anything a request is sent to the server to change its position.
     * The server may later respond, and the RHL will do the rest of the job
     * (actually setting the position of this button to be the requested pos).
     *
     * LOGIC
     * -If the left mouse button is not pressed, the mouse motion event performs a
     * raycast on the fieldBox and if the ray intersects it the buttonPosShape x
     * and z position are set to be the x and z of the intersection point (so that
     * the current height is preserved). In physicsWorld, a disc shape is added
     * in the [x,z] position and checked whether it intersects anything else. If
     * it does the material of the buttonPosShape is set to invalidPosMat to
     * show that the position is not valid. Otherwise it is set to validPosMat.
     * -If the left mouse button is pressed, and the distance between [x,y] and
     * [px, py] is lower than distanceThreshold, then the buttonPosShape material
     * is set to setPosMat, otherwise it is set to giveupPosMat.
     * -If the user presses the left button the field pressed is set to true and
     * the px and py are set to be the click position.
     * -If the user releases the mouse button and pressed was previously true,
     * first check whether the position is valid and that the distance between
     * [x,y] and [px,py] is lower than distanceThreshold. If these conditions are
     * satisfied, send a set position request to the server, and set pressed to
     * false.
     */
    class SetPositionListener implements RawInputListener
    {
        private int x, y;

        public SetPositionListener()
        {
            x = y = 0;
        }

        public void onMouseMotionEvent(MouseMotionEvent evt)
        {
            x = evt.getX();
            y = evt.getY();
            //Raycast the field box
            //buttonShape.setLocalTranslation(new Vector3f(x/10f, 0.1f, y/10f));
        }

        public void onMouseButtonEvent(MouseButtonEvent evt)
        {
            if(evt.isReleased())
            {
                //raycast the field box
                Vector3f cPos = graphicsWorld.screenRayCastField(new Vector2f(x, y));
                
                try { //Send request to server
                    if(cPos != null)
                    {
                        ClientInfo ci = new ClientInfo(client.getClientInfo());
                        ci.setStartXY(cPos.x, cPos.z);
                        ci.setType(ClientInfo.Type.PLAYER);
                        client.write(ci);
                    }
                    else
                    {
                        ClientInfo ci = new ClientInfo(client.getClientInfo());
                        ci.setType(ClientInfo.Type.SPECTATOR);
                        client.write(ci);
                    }
                } catch (IOException ex) {
                    ex.printStackTrace();
                }
            }
        }

        /*unused*/
        public void onJoyAxisEvent(JoyAxisEvent evt) {}
        public void onJoyButtonEvent(JoyButtonEvent evt) {}
        public void onKeyEvent(KeyInputEvent evt) {}
    }

    /**
     * RawInputListener to be used while the game is running. It stores each
     * mouse displacement inside a BlockingQueue. This queue is then processed
     * by the SendInputThread a few times per second and the input is finally
     * sent to the server.
     */
    private class CursorDisplacementListener implements RawInputListener
    {
        private SendInputThread sendInputThread;
        private boolean hasFocus;

        public CursorDisplacementListener(SendInputThread sendInputThread) {
            this.sendInputThread = sendInputThread;
            hasFocus = false;
        }
        
        public void onMouseMotionEvent(MouseMotionEvent evt)
        {
            if(hasFocus)
            {
                Point disp = new Point(evt.getDX(), -evt.getDY());
                sendInputThread.mouseDisplaced(disp);
            }
        }

        public void onMouseButtonEvent(MouseButtonEvent evt)
        {
            if(evt.getButtonIndex() == 0 && evt.isPressed())
            {
                hasFocus = true;
                graphicsWorld.getInputManager().setCursorVisible(false);
            }
            else if(evt.getButtonIndex() == 1 && evt.isPressed())
            {
                hasFocus = false;
                graphicsWorld.getInputManager().setCursorVisible(true);
            }
        }

        /*unused*/
        public void onJoyAxisEvent(JoyAxisEvent evt) {}
        public void onJoyButtonEvent(JoyButtonEvent evt) {}
        public void onKeyEvent(KeyInputEvent evt) {}
    }


    private class SendInputThread implements Runnable
    {
        private BlockingQueue<Point> displacementQueue;
        private int stepsPerSecond;
        private volatile boolean running;

        public SendInputThread() {
            this.displacementQueue = new LinkedBlockingQueue<Point>();
            stepsPerSecond = 20;
            running = true;
        }

        public void mouseDisplaced(Point disp)
        {
            boolean added = displacementQueue.offer(disp);
            if(!added) //something really bad happened eh
                System.err.println("Failed to add new displacement object into Game.displacementQueue");
        }

        public void run()
        {
            running = true;
            
            while(running)
            {
                //Store all the displacements recorded till now in this Collection
                Collection<Point> displacement = new ArrayList<Point>();

                //First take one element, so that it will block this thread until
                //one element is added to the displacementQueue
                try {
                    displacement.add(displacementQueue.take());
                } catch (InterruptedException ex) {
                    ex.printStackTrace();
                }

                //Then take the rest
                displacementQueue.drainTo(displacement);

                //Compute the average displacement
                int tx = 0, ty = 0;

                for(Point p: displacement)
                {
                    tx += p.x;
                    ty += p.y;
                }

                Point avgDisp = new Point(tx/displacement.size(), ty/displacement.size());
                
                Vec2 force = physicsThread.getPhysicsWorld().
                        computeForceFromDisplacement(avgDisp.x, avgDisp.y);
                physicsThread.getPhysicsWorld().applyTimedForceOnBody(
                        client.getClientInfo().getName(), force, (long)(1000/stepsPerSecond));
                     
                Input input = new Input(avgDisp);
                try {
                    client.write(input);
                } catch (IOException ex) {
                    ex.printStackTrace();
                }

                try {
                    Thread.sleep(1000/stepsPerSecond);
                } catch (InterruptedException ex) {
                    ex.printStackTrace();
                }
            }
        }

        public int getStepsPerSecond() {
            return stepsPerSecond;
        }

        public void setStepsPerSecond(int stepsPerSecond) {
            this.stepsPerSecond = stepsPerSecond;
        }

        public void stop() {
            running = false;
            //add an element to the queue to unblock the main loop if it is blocked
            displacementQueue.add(new Point());
        }
    }
}
